Understanding Lisk's Transactions Signing Scheme

Flávio Codeço Coelho


In [2]:
from hashlib import sha256
import json
import ed25519

EdDSA

EdDSA is a variant of Schnorr signatures. Lisk uses ed25519 to sign off transactions. EdDSA is based on Edwards elliptic curves over a finite field: $$E(\mathbb{F}_q)$$ where q is a large prime number, $$q=2^{255}-19$$

Creating a key pair

Based on Lisk documentation about key pairs, we start from a BIP-39 passphrase:


In [3]:
passphrase = b"witch collapse practice feed shame open despair creek road again ice least"
H = sha256(passphrase)
H.hexdigest()


Out[3]:
'77d2ad92c611e5064cf22528ffe6e9cab93a80567b8516febc9b5b7b1616acc6'

In [4]:
H.digest()


Out[4]:
b'w\xd2\xad\x92\xc6\x11\xe5\x06L\xf2%(\xff\xe6\xe9\xca\xb9:\x80V{\x85\x16\xfe\xbc\x9b[{\x16\x16\xac\xc6'

First the private key:


In [5]:
sk = ed25519.SigningKey(H.digest())
sk.to_bytes()


Out[5]:
b'w\xd2\xad\x92\xc6\x11\xe5\x06L\xf2%(\xff\xe6\xe9\xca\xb9:\x80V{\x85\x16\xfe\xbc\x9b[{\x16\x16\xac\xc6\xee)"9\xe8\x93\xd6\xa8\xbc!~\x9d\xafD\xfb\x02\x81\x83`b]S9{\x9d\xf0\xef\xaa\\c(\xa3'

then the public:


In [6]:
vk = sk.get_verifying_key()
vk.to_bytes()


Out[6]:
b'\xee)"9\xe8\x93\xd6\xa8\xbc!~\x9d\xafD\xfb\x02\x81\x83`b]S9{\x9d\xf0\xef\xaa\\c(\xa3'

Signing a transaction

Let $tx$ be a fund transfer transaction (type 0 in lisk):


In [7]:
tx = {
    "type": 0,
    "amount": 128,
    "senderPublicKey": "Public key of the sender",
    "timestamp": "<Timestamp>",
    "recipientId": "<Id of the recipient>",
    "signature": "<Signature of the data block>",
    "id": "<txid>",
    "fee": 10000000,
    "senderId": "<Id of the sender>",
}
txJSON = json.dumps(tx)
txJSON


Out[7]:
'{"type": 0, "amount": 128, "senderPublicKey": "Public key of the sender", "timestamp": "<Timestamp>", "recipientId": "<Id of the recipient>", "signature": "<Signature of the data block>", "id": "<txid>", "fee": 10000000, "senderId": "<Id of the sender>"}'

Let $H_{tx}$ be the SHA256 hash of the transaction block.


In [8]:
Htx = sha256(txJSON.encode('ascii'))
Htx.digest()


Out[8]:
b'|J\xdb\xd5\x88A\xf54\x1fI\x9d\x96\xba\xb5\xb8\x97\n/\xa2n\x1c\x87i\xc3\x99\x02!\xa1Q\xf9L '

Lisk then signs $H_{tx}$ using the sender keys:


In [9]:
sig = sk.sign(Htx.digest(), encoding='hex')
sig


Out[9]:
b'af835a49a385a0aaf6574395a1fc34bd868018a2cfec6f59cf14495560b2f142257fa98fabdf67cf8cd6540631befda2fbb312172286dcd9759e335be0c37b0d'

Adding the signature to the transaction

The signature is then added to the datablock. Ed25519 signatures are not malleable, meaning that for the same private key and message (Tx), there is only one valid signature.


In [10]:
tx['signature'] = sig.decode('ascii')
tx2JSON = json.dumps(tx)
tx


Out[10]:
{'type': 0,
 'amount': 128,
 'senderPublicKey': 'Public key of the sender',
 'timestamp': '<Timestamp>',
 'recipientId': '<Id of the recipient>',
 'signature': 'af835a49a385a0aaf6574395a1fc34bd868018a2cfec6f59cf14495560b2f142257fa98fabdf67cf8cd6540631befda2fbb312172286dcd9759e335be0c37b0d',
 'id': '<txid>',
 'fee': 10000000,
 'senderId': '<Id of the sender>'}

Second signature

If a second signature is enabled for the sender's account, the transaction can receive a second signature.

Let $I_{tx}$ be the SHA256 hash of the transaction block appended with the first signature. Again we will sign $I_{tx}$ instead of the transaction block.


In [11]:
Itx = sha256(tx2JSON.encode('ascii'))
sig2 = sk.sign(Itx.digest(), encoding='hex')
sig2


Out[11]:
b'e1530a6a340ba83485de87a16ea96066c621cf95cb07b1ef07bd1e88fc95a232213855911357ea3c1a0bd863a1506d8e99022f46907114f196402cb27c7d8209'

Verifying


In [14]:
vk.verify(sig2,Itx.digest(),encoding='hex')

Computing the Transaction ID

The ID of the transaction (TxID) after the second signature is computed from the SHA256 of the transaction block with the signature(s) appended. The ID is the first 8 bytes of this hash, reversed. Therefore,

  • TxID is unique since its based on the hash of the transaction block plus the signature(s).
  • EdDSA signatures are deterministic. Elininating possible attacks from signatures generated with weak random numbers.
  • Signature verification is also deterministic.
  • TxID Uniqueness depends on the collision resistance of SHA256.
  • Verifying if the same ID is already present in the blockchain can prevent replay attacks (Where the exact same transaction would be re-inserted)

HashEdDSA vs PureEdDSA

Lisk uses the Hashed version of EdDSA, where the message to be signed, is prehashed ($\mathtt{PH}(Tx) = \mathtt{SHA256}(Tx)$) and then signed. This has some advantages:

  • HashEdDSA requires only a single pass over the input ($\mathtt{PH}(Tx)$).
  • Hashing adds collision resistance, even though PureEdDSA is resilient to collisions of the hash.
  • The RFC for Ed25519 recommends using SHA512 for prehashing, Lisk uses SHA256.

and disadvantages:

  • Introduces potential vulnerabilities of the prehash function.
  • Not resilient to collisions
  • Requires authentication of signature scheme (Hash or Pure), to avoid cross-protocol attacks